/**
 * 
 */
package neptune.manager.configurator;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import neptune.manager.builder.IPAddressRange;
import neptune.manager.model.ApplicationProperties;
import neptune.manager.model.topology.EndPoint;
import neptune.manager.model.topology.IPAddress;
import neptune.manager.model.topology.Link;
import neptune.manager.model.topology.NetworkInterface;
import neptune.manager.model.topology.Node;
import neptune.manager.model.topology.Property;
import neptune.manager.model.topology.Topology;
import neptune.manager.model.topology.UndefinedControlInterfaceException;
import neptune.manager.storage.StorageReadException;
import neptune.manager.storage.XMLApplicationPropertiesStorage;
import neptune.manager.utils.SSHCommandException;
import neptune.manager.utils.SSHConnectionHandler;


/**
 * @author Roberto Bifulco [info@robertobifulco.it, robertobifulco.it]
 * @author Luigi Lorenzo Arpino [luigiarpino@yahoo.it]
 * 
 */
public class SSHConfigurator implements Configurator {

	/**
	 * Classe innestata di SSHConfigurator per gestire la sola configurazione di
	 * un Link
	 * 
	 * @author Roberto Bifulco [info@robertobifulco.it, robertobifulco.it]
	 * 
	 */
	public class SSHLinkConfigurator {

		private final Link link;
		private final Node source;
		private final Node destination;

		public SSHLinkConfigurator(Link link, Node source, Node destination) {
			this.link = link;
			this.source = source;
			this.destination = destination;
		}

		public void configureLink() throws UnknownHostException, IOException,
				UndefinedControlInterfaceException, SSHCommandException,
				ConfiguratorException {

			EndPoint sourceEp = link.getSource();
			NetworkInterface sourceNI = null;
			// Recupero dell'interfaccia che rappresenta l'end point source
			for (int i = 0; i < source.getInterfaces().length; i++) {
				if (sourceEp.isThisEndPoint(source.getInterfaces()[i])) {
					sourceNI = source.getInterfaces()[i];
				}
			}

			configureEndPoint(source, sourceEp, sourceNI, link, destination);

			// Configurazione della destinazione, se il link e' full duplex
			if (link.isFullDuplex()) {
				EndPoint destinationEp = link.getDestination();
				NetworkInterface destinationNI = null;

				// Recupero dell'interfaccia che rappresenta l'end point
				// destination
				for (int i = 0; i < destination.getInterfaces().length; i++) {
					if (destinationEp.isThisEndPoint(destination
							.getInterfaces()[i])) {
						destinationNI = destination.getInterfaces()[i];
					}
				}

				configureEndPoint(destination, destinationEp, destinationNI,
						link, source);
			}
		}

		/**
		 * Configura un link per il dato endPoint.
		 * 
		 * @param sourceNode
		 *            il nodo corrispondente all'endPoint
		 * @param actualEndPoint
		 * @param netIf
		 *            l'interfaccia su cui e' attaccato l'endPoint
		 * @param link
		 *            di cui l'endPoint e' uno degli estremi
		 * @param destNode
		 *            il nodo corrispondente all'altro endPoint di questo link
		 * @throws UnknownHostException
		 * @throws IOException
		 * @throws SSH2Exception
		 * @throws UndefinedControlInterfaceException
		 * @throws SSHCommandException
		 * @throws ConfiguratorException
		 */
		public void configureEndPoint(Node sourceNode, EndPoint actualEndPoint,
				NetworkInterface netIf, Link link, Node destNode)
				throws UnknownHostException, IOException,
				UndefinedControlInterfaceException, SSHCommandException,
				ConfiguratorException {

			// Ssh connection to source
			SSHConnectionHandler sshConnection = SSHConnectionHandler
					.createSSHConnectionHandler(sourceNode.getControlAddress()
							.toString(), sourceNode.getControlPort(),
							sourceNode.getOperativeSystem().getAdminUsername(),
							sourceNode.getOperativeSystem().getAdminPassword());

			switch (actualEndPoint.getMultiplexingTech()) {
			case Link.ONE_LINK_PER_INTERFACE:
				// Inizializzazione cancellando la qdisc e creando il device
				sshConnection.sendCommandNoErrorControl(SSHCommandsUtils
						.getDeleteQdicsCommand(netIf.getInterfaceName()));

				// Impostazione property
				if (link.getProperties() != null) {
					sshConnection.sendCommand(SSHCommandsUtils
							.getCreateQdiscCommand(netIf.getInterfaceName(),
									link.getProperties()));

					// Bandwidth
					for (Property prop : link.getProperties()) {
						if (prop.getName().equals(Property.BANDWIDTH)) {
							String[] bandwidthCmds = SSHCommandsUtils
									.getHTBCommands(netIf.getInterfaceName(),
											prop.getValue());

							sshConnection.sendCommand(bandwidthCmds[0]);
							sshConnection.sendCommand(bandwidthCmds[1]);
						}
					}
				}

				EndPoint destinationEndPoint = null;
				if (link.getSource().equals(actualEndPoint))
					destinationEndPoint = link.getDestination();
				else
					destinationEndPoint = link.getSource();

				configureRouteTable(sshConnection, destinationEndPoint,
						actualEndPoint, destNode);

				// Closing connection
				sshConnection.dispose();
				break;

			default:
				break;
			}
		}

		/**
		 * Invia i comandi di configurazione della tabella di forwarding alla
		 * connessione passata come argomento.<br>
		 * 
		 * @param handler
		 * @param destinationEndPoint
		 *            e' l'endPoint destinazione, verso cui deve essere
		 *            configurata una route
		 * @param sourceEndPoint
		 *            e' l'endPoint corrispondente al nodo su cui vengono
		 *            inviati i comandi
		 * @param topology
		 * @throws ConfiguratorException
		 * @throws SSHCommandException
		 */
		public void configureRouteTable(SSHConnectionHandler handler,
				EndPoint destinationEndPoint, EndPoint sourceEndPoint,
				Node destNode) throws ConfiguratorException,
				SSHCommandException {

			NetworkInterface destIf = destNode
					.getInterfaceByName(destinationEndPoint.getInterfaceId());

			if (destIf.getIpAddresses().length != 1) {
				throw new ConfiguratorException(
						"Only OLPI multiplexing tech is supported. "
								+ "Exactly one IP address for interface must be defined");
			}

			IPAddress destAddress = destIf.getIpAddresses()[0];

			String command = SSHCommandsUtils.getAddRouteCommand(destAddress
					.getSubnetAddressString(), destAddress.getSubnetMask(),
					sourceEndPoint.getInterfaceId());

			handler.sendCommand(command);

		}
	}

	private IPAddressRange controlIpRange;

	public SSHConfigurator(IPAddressRange controlIpRange) {
		super();
		this.controlIpRange = controlIpRange;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * neptune.manager.configurator.Configurator#configureTopology(neptune.manager
	 * .model.Topology)
	 */
	public void configureTopology(Topology topology)
			throws ConfiguratorException 
	{	
		if (this.isAllNodesReady(topology))
		{	
			// TODO configurazione dei nodi (come impostare pwd amministratore)
			configureNodes(topology);
			// Configurazione dei link
			resetLinkConfiguration(topology);
			configureLinks(topology);
			String ipGateway=getIpGateway();
			configureGateway(topology, ipGateway);
		}
		else
		{
			String msg="\n Unable to Configure Topology!";
			msg += "\n Some virtual machine are not ready.";
			msg += "\n Please reapeat the enforce topology after few minutes.";
			throw new  ConfiguratorException(msg); 
		}
		
	}
	
	private String getIpGateway() throws ConfiguratorException
	{
		ApplicationProperties appProp;
		try 
		{
			// Leggo il gateway delle macchine virtuali
			String pathFile = System.getProperty("user.home")+File.separator+".emuPlugin"+File.separator+"configuration.xml";
			File xmlProperties=new File(pathFile);
			XMLApplicationPropertiesStorage appPropStorage=new XMLApplicationPropertiesStorage(xmlProperties);
			appProp = appPropStorage.getApplicationProperties();
		}
		catch (StorageReadException e) 
		{
			throw new ConfiguratorException(e);
		}
		return appProp.getGatewayControlIpRange();
	}
	
	/**
	 * Effettua la configurazione dei gateway delle macchine virtuali
	 * 
	 * @param topology
	 * @throws ConfiguratorException 
	 */
	private void configureGateway(Topology topology, String ipGateway) throws ConfiguratorException 
	{
		for (Node node : topology.getNodes()) 
		{
			try 
			{
				SSHConnectionHandler sshConn = SSHConnectionHandler
						.createSSHConnectionHandler(node.getControlAddress()
						.getAddress(), node.getControlPort(), node
						.getOperativeSystem().getAdminUsername(), node
						.getOperativeSystem().getAdminPassword());
				String command = SSHCommandsUtils.getAddDefaultGatewayCommand(ipGateway);
				sshConn.sendCommand( command ); 
				sshConn.dispose();
			} 
			catch (IOException e) 
			{
				throw new ConfiguratorException(e);
			}
			catch (UndefinedControlInterfaceException e) 
			{
				throw new ConfiguratorException(e);
			}
			catch (SSHCommandException e) 
			{
				throw new ConfiguratorException(e);
			}
		}
	}

	/**
	 * Restituisce true se tutte le macchine virtuali presenti nella topologia
	 * hanno il server SSH attivo
	 * @param topology
	 * @return
	 * @throws ConfiguratorException
	 */
	private boolean isAllNodesReady(Topology topology)throws ConfiguratorException
	{
		try
		{
			for (Node node : topology.getNodes())
			{
				SSHConnectionHandler sshConn = SSHConnectionHandler
				.createSSHConnectionHandler(node.getControlAddress()
						.getAddress(), node.getControlPort(), node
						.getOperativeSystem().getAdminUsername(), node
						.getOperativeSystem().getAdminPassword());
				sshConn.sendCommand("ls");
				sshConn.dispose();
			}
			return true;
		}
		catch (UnknownHostException e) 
		{
			return false;
		}
		catch (IOException e) 
		{
			return false;
		} 
		catch (UndefinedControlInterfaceException e) 
		{
			return false;
		}
		catch (SSHCommandException e) 
		{
			return false;
		}
	}

	
	
	private void configureNodes(Topology topology) throws ConfiguratorException {
		for (Node node : topology.getNodes()) {
			try {
				SSHConnectionHandler sshConn = SSHConnectionHandler
						.createSSHConnectionHandler(node.getControlAddress()
								.getAddress(), node.getControlPort(), node
								.getOperativeSystem().getAdminUsername(), node
								.getOperativeSystem().getAdminPassword());

				for (NetworkInterface netIf : node.getInterfaces()) {
					// Se l'interfaccia e' eth0, non viene configurata poiche' e
					// configurata durante l'allocazione.
					if (netIf.getInterfaceName().equals("eth0"))
						continue;

					if (netIf.getIpAddresses().length != 1) {
						throw new ConfiguratorException(
								"Only OLPI multiplexing tech is supported. "
										+ "Exactly one IP address for interface must be defined");
					}

					IPAddress ifAddr = netIf.getIpAddresses()[0];

					String command = SSHCommandsUtils.getSetInterfaceIP(netIf
							.getInterfaceName(),
							ifAddr.getAddress().toString(), ifAddr
									.getSubnetMask());
					sshConn.sendCommand(command);
				}

				sshConn.dispose();

			} catch (UnknownHostException e) {
				throw new ConfiguratorException(e);
			} catch (IOException e) {
				throw new ConfiguratorException(e);
			} catch (UndefinedControlInterfaceException e) {
				throw new ConfiguratorException(e);
			} catch (SSHCommandException e) {
				throw new ConfiguratorException(e);
			}
		}
	}

	private void configureLinks(Topology topology) throws ConfiguratorException {
		Link[] links = topology.getLinks();
		for (int i = 0; i < links.length; i++) {
			// Recupera i nodi che contengono gli end point del link
			Node source = null;
			Node destination = null;
			for (int j = 0; j < topology.getNodes().length; j++) {
				Node actual = topology.getNodes()[j];
				for (int k = 0; k < actual.getInterfaces().length; k++) {
					if (links[i].getSource().isThisEndPoint(
							actual.getInterfaces()[k]))
						source = actual;
					if (links[i].getDestination().isThisEndPoint(
							actual.getInterfaces()[k]))
						destination = actual;
				}
			}

			try {
				SSHLinkConfigurator linkConfigurator = new SSHLinkConfigurator(
						links[i], source, destination);
				linkConfigurator.configureLink();
			} catch (UnknownHostException e) {
				throw new ConfiguratorException(e);
			} catch (IOException e) {
				throw new ConfiguratorException(e);
			} catch (UndefinedControlInterfaceException e) {
				throw new ConfiguratorException(e);
			} catch (SSHCommandException e) {
				throw new ConfiguratorException(e);
			}
		}
	}

	/**
	 * Imposta su tutti i nodi della topologia che le uniche rotte nella tabella
	 * di routing siano quelle che permettono l'accesso alla rete di controllo.<br>
	 * <br>
	 * Il metodo semplicemente accede ad ogni nodo della topologia e, tramite
	 * comando "route", elimina tutte le rotte il cui indirizzo IP destinazione
	 * non si trova sulla subnet di uno qualsiasi dei range di indirizzi IP
	 * della rete di controllo.
	 * 
	 * @param topology
	 * @throws ConfiguratorException
	 */
	private void resetLinkConfiguration(Topology topology)
			throws ConfiguratorException {
		Node[] nodes = topology.getNodes();
		for (Node node : nodes) {
			try {
				SSHConnectionHandler handler = SSHConnectionHandler
						.createSSHConnectionHandler(node.getControlAddress()
								.toString(), node.getControlPort(), node
								.getOperativeSystem().getAdminUsername(), node
								.getOperativeSystem().getAdminPassword());

				String[][] routes = getNodeRoutes(handler);

				for (String[] route : routes) {
					String destAddress = route[0];
					boolean isControl = false;

					for (IPAddress[] range : controlIpRange.getRanges()) {
						IPAddress subnet = range[0].getSubnetAddress();

						if (destAddress.matches(".+\\..+\\..+\\..+"))
							if (subnet.include(destAddress)) {
								isControl = true;
								break;
							}
					}

					if (!isControl) {
						String command = SSHCommandsUtils
								.getDelRouteCommand(route);
						handler.sendCommand(command);
					}
				}

			} catch (UnknownHostException e) {
				throw new ConfiguratorException(e);
			} catch (IOException e) {
				throw new ConfiguratorException(e);
			} catch (UndefinedControlInterfaceException e) {
				throw new ConfiguratorException(e);
			} catch (SSHCommandException e) {
				throw new ConfiguratorException(e);
			}
		}
	}

	private String[][] getNodeRoutes(SSHConnectionHandler handler)
			throws SSHCommandException {
		handler.sendCommand(SSHCommandsUtils.getRouteCommand());

		BufferedReader reader = handler.getStdOut();
		String line;

		int i = 0;
		try {

			ArrayList<String[]> routes = new ArrayList<String[]>(0);

			while ((line = reader.readLine()) != null) {
				if (i > 1) { // elimina le prime due righe restituite
					// dall'output
					String[] result = new String[8];
					String[] splitRes = line.split(" ");
					int j = 0;
					for (String split : splitRes) {
						if (!split.equals("")) {
							result[j] = split;
							j++;
						}
					}
					routes.add(result);
				}
				i++;
			}

			return routes.toArray(new String[][] {});
		} catch (IOException e) {
			throw new SSHCommandException(e);
		}
	}
	

}
